﻿using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Windows.Markup;
using SL5.Common.Expressions;

namespace System.Windows.Data
{
	using Expression = System.Linq.Expressions.Expression;

	/// <summary>
	/// Represents an object data provider which can be used to call parameterized static methods or constructors.
	/// </summary>
	public sealed class ObjectDataProvider : MarkupExtension
	{
		#region Private Variables 
		private readonly Collection<object> _constructorParameters = new Collection<object>();
		private readonly Collection<object> _methodParameters = new Collection<object>();
		#endregion Private Variables 

		#region Public Properties 
		/// <summary>
		/// Gets the parameters to pass to the constructor invocation.
		/// </summary>
		public Collection<object> ConstructorParameters
		{
			get
			{
				return this._constructorParameters;
			}
		}

		/// <summary>
		/// Gets or sets the name of the static method to invoke.  Left blank, the <see cref="ObjectDataProvider" /> will instead look for a constructor to invoke.
		/// </summary>
		public string MethodName
		{
			get;
			set;
		}

		/// <summary>
		/// Gets the parameters to pass to the method invocation.
		/// </summary>
		public Collection<object> MethodParameters
		{
			get
			{
				return this._methodParameters;
			}
		}

		/// <summary>
		/// Gets or sets the type of the object to invoke the method on.  If no method name is specified, then a constructor on the type will be invoked.
		/// </summary>
		public Type ObjectType
		{
			get;
			set;
		}
		#endregion Public Properties 

		#region Private Methods 
		/// <summary>
		/// Creates a lambda expression body in order to invoke the method/constructor specified.
		/// </summary>
		/// <returns>Expression.</returns>
		private Expression CreateExpression()
		{
			if (this.ObjectType == null)
			{
				throw new InvalidOperationException("Can not provide value without an object type.");
			}
			if (this.ObjectType.IsInterface || this.ObjectType.IsGenericTypeDefinition)
			{
				throw new InvalidOperationException("Can not provide value with an object type that is an interface or generic type definition.");
			}
			MethodData methodData;
			if (String.IsNullOrEmpty(this.MethodName))
			{
				if (this.ObjectType.IsAbstract)
				{
					throw new InvalidOperationException("Can not create an instance of an abstract class.");
				}
				if (this.ObjectType.IsValueType && this.ConstructorParameters.Count == 0)
				{
					return Expression.Default(this.ObjectType);
				}
				methodData = ExpressionHelper.SelectBestOverload(
					this.ObjectType.GetConstructors(), 
					this.ConstructorParameters
				);
			}
			else
			{
				methodData = ExpressionHelper.SelectBestOverload(
					this.ObjectType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(method => method.Name == this.MethodName && method.ReturnType != typeof(void) && !method.IsGenericMethodDefinition),
					this.MethodParameters
				);
			}
			if (methodData == null)
			{
				throw new InvalidOperationException("Failed to resolve the correct method on " + this.ObjectType);
			}
			return methodData.CreateExpression();
		}
		#endregion Private Methods 

		#region Public Methods 
		/// <summary>
		/// When implemented in a derived class, returns an object that is provided as the value of the target property for this markup extension.
		/// </summary>
		/// <param name="serviceProvider">A service provider helper that can provide services for the markup extension.</param>
		/// <returns>The object value to set on the property where the extension is applied.</returns>
		public override object ProvideValue(IServiceProvider serviceProvider)
		{
			Expression body = this.CreateExpression();
			return Expression.Lambda<Func<object>>(
				!body.Type.IsValueType ? body : Expression.Convert(
					body,
					typeof(object)
				)
			).Compile().Invoke();
		}
		#endregion Public Methods 
	}
}